"""
  Copyright 2008 Texas A&M University Licensed under the
  Educational Community License, Version 2.0 (the "License"); you may
  not use this file except in compliance with the License. You may
  obtain a copy of the License at
  
  http://www.osedu.org/licenses/ECL-2.0

  Unless required by applicable law or agreed to in writing,
  software distributed under the License is distributed on an "AS IS"
  BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
  or implied. See the License for the specific language governing
  permissions and limitations under the License.
"""

import datetime
import time

from lxml import etree

from twisted.internet import defer
from twisted.web.resource import Resource
from twisted.web.server import NOT_DONE_YET
from twisted.python import log

from meercat.storage.interfaces import IStorage
from meercat.mapreduce import mapreduce
from meercat.mapreduce.interfaces import IMapper

from meercat.server.oai import tags, constants
from meercat.server.oai.compat import strptime, temporary_id



class OAIRepository(Resource):
    isLeaf = True
    
    def __init__(self, storage, repositoryName, adminEmail, description=[]):
        Resource.__init__(self)
        
        #TODO: Add validation to incoming parameters
        self._repositoryName = repositoryName
        self._adminEmail = adminEmail
        self._description = description
        self._protocolVersion = u'2.0'
        self._deletedRecord = u'persistent'
        self._granularity = u'YYYY-MM-DDThh:mm:ssZ'
        #TODO: Calculate this from the storage
        self._earliestDatestamp = datetime.datetime(1900,1,1)
        self._tokens = {}
        self._storage = storage
    
    def render(self, request):
        """
        request.method
        request.args dictionary of lists
        request.requestHeaders.getRawHeaders(name='')
        """
        request.setHeader('Content-Type','text/xml')
        
        root = tags.OAIPMH()
        root.append(tags.ResponseDate())
        
        request_tag = tags.Request(request)
        root.append(request_tag)
        verb = None
        
        d = None
        
        if not 'verb' in request.args:
            root.append(tags.Error('badVerb', freetext=u'A verb is required'))
        elif len(request.args['verb']) != 1:
            root.append(tags.Error('badVerb',
                                   freetext=u'Only 1 verb is allowed'))
            del request.args['verb']
        elif not request.args['verb'][0] in constants.verbs:
            root.append(tags.Error('badVerb', freetext=u'verb must be one of: '
                                   '%s' % (', '.join(constants.verbs),)))
            del request.args['verb']
        else:
            verb = request.args['verb'][0]
            f = getattr(self, 'render_%s' % verb, None)
            
            if not f or not callable(f):
                root.append(tags.Error('badVerb',
                                       freetext=u'%s is not a supported verb' % (verb,)))
            else:
                request_tag.set(u'verb', verb)
                d = defer.maybeDeferred(f,request, root, request_tag)
        
        if d is None:
            return etree.tostring(root, encoding='utf-8')
        
        def callback(result):
            request.write(etree.tostring(root, encoding='utf-8'))
            request.finish()
            return result
        
        def errorback(fail):
            log.err('render errorback: %s' % (str(fail),))
            request.write(str(fail))
            request.finish()
            return fail
        
        d.addCallbacks(callback, errorback)
        return NOT_DONE_YET
    
    def _write(self,request, etree):
        request.write(etree.tostring(root, encoding='utf-8'))
        request.finish()
    
    def _renderResource(self, resource, container, format):
        record = etree.Element('record')
        container.append(record)
        
        self._renderResourceHeader(resource,record)
        # map/reduce metadata
        
        d = mapreduce(input=resource.getMetadata(),targetFormat=format)
        
        def callback(result):
            if result is not None:
                metadata = etree.Element('metadata')
                record.append(metadata)
                metadata.append(etree.fromstring(result.getValue()))
            return result
        
        d.addCallback(callback)
        
        return d
    
    def _renderResourceHeader(self, resource, container):
        header = etree.Element('header')
        container.append(header)
        identifier = etree.Element('identifier')
        datestamp  = etree.Element('datestamp')
        header.append(identifier)
        header.append(datestamp)
        
        identifier.text = str(resource.getIdentifier())
        datestamp.text =  resource.getLastModified().strftime(constants.datetime_format)
        
        if not resource.isActive():
            header.set(u'status',u'deleted')
    
    def _listResources(self, request, root, request_tag):
        kwargs = {}
        resumptionToken = None
        
        format_string = constants.granularities[self._granularity]
        
        errors = False
        
        if 'set' in request.args:
            errors = True
            root.append(tags.Error('noSetHierarchy',
                                   freetext=u'This repository does not '
                                   'currently support sets'))
        
        if 'resumptionToken' in request.args:
            if len(request.args['resumptionToken']) != 1:
                errors = True
                root.append(tags.Error('badArgument',
                                       freetext=u'Only 1 resumptionToken is allowed'))
            
            token = self._tokens.get(request.args['resumptionToken'][0],None)
            
            if token is None:
                errors = True
                root.append(tags.Error('badResumptionToken',
                                       freetext=u'The resumption token, %s, is '
                                       'invalid' % (str(request.args['resumptionToken'][0]),)))
            elif token.expired:
                errors = True
                root.append(tags.Error('badResumptionToken',
                                       freetext=u'The resumption token, %s, has '
                                       'expired' % (str(token.id))))
                del self._tokens[token.id]
        else:
            if 'from' in request.args:
                if len(request.args['from']) > 1:
                    errors = True
                    root.append(tags.Error('badArgument',
                                           freetext=u'Only 1 from argument is '
                                           'allowed'))
                elif len(request.args['from']) == 1:
                    try:
                        kwargs['from_date'] = strptime(request.args['from'],
                                                       fromat_string)
                        request_tag.set('from',request.args['from'])
                    except:
                        errors = True
                        root.append(tags.Error('badArgument',
                                               freetext=u'Invalid from format'))
            
            if 'until' in request.args:
                if len(request.args['until']) > 1:
                    errors = True
                    root.append(tags.Error('badArgument',
                                           freetext=u'Only 1 until argument is '
                                           'allowed'))
                elif len(request.args['until']) == 1:
                    try:
                        kwargs['until_date'] = strptime(request.args['until'],
                                                        format_string)
                        request_tag.set('until',request.args['until'])
                    except:
                        errors = True
                        root.append(tags.Error('badArgument',
                                               freetext=u'Invalid until format'))
            
            token = StorageWrapper(self._storage, **kwargs)
            self._tokens[token.id] = token
        
        if not errors:       
            token_tag, resources = token.getNextSet()
            
            if len(resources) == 0 and token.getTimesCalled() == 1:
                errors = True
                root.append(tags.Error('noRecordsMatch'))
        
        if not errors:
            return token_tag, resources
        
        return None, []
    
    def render_GetRecord(self, request, root, request_tag):
        identifier = None
        metadataPrefix = None
        
        if not 'identifier' in request.args:
            root.append(tags.Error('badArgument',
                                   freetext=u'An identifier must be provided'))
        elif len(request.args['identifier']) != 1:
            root.append(tags.Error('badArgument',
                                   freetext=u'Please provide only 1 identifier'))
        else:
            identifier = request.args['identifier'][0]
        
        if not 'metadataPrefix' in request.args:
            root.append(tags.Error('badArgument',
                                   freetext=u'A metadataPrefix must be provided'))
        elif len(request.args['metadataPrefix']) != 1:
            root.append(tags.Error('badArgument',
                                   freetext=u'Please provide only 1 metadataPrefix'))
        else:
            metadataPrefix = request.args['metadataPrefix'][0]
        
        if identifier is None or metadataPrefix is None:
            return
        
        s = self._storage
        resources = s.getResources(identifier=identifier, inactive=True)
        if len(resources) == 0:
            root.append(tags.Error('idDoesNotExist',
                                   freetext=u'%s does not exist in '
                                   '%s' % (str(identifier),
                                           str(self._repositoryName))))
            return
        
        container = etree.Element(u'GetRecord')
        root.append(container)
        request_tag.set(u'identifier', identifier)
        request_tag.set(u'metadataPrefix', metadataPrefix)
        return self._renderResource(resources[0],container, metadataPrefix)
    
    def render_Identify(self, request, root, request_tag):
        container = etree.Element(u'Identify')
        root.append(container)
        
        # add repositoryName
        temp = etree.Element(u'repositoryName')
        temp.text = self._repositoryName
        container.append(temp)
        
        # add baseURL
        temp = etree.Element(u'baseURL')
        temp.text = request.path
        container.append(temp)
        
        # add protocolVersion
        temp = etree.Element(u'protocolVersion')
        temp.text = self._protocolVersion
        container.append(temp)
        
        # add adminEmail
        temp = etree.Element(u'adminEmail')
        temp.text = self._adminEmail
        container.append(temp)
        
        # add earliestDatestamp
        temp = etree.Element(u'earliestDatestamp')
        temp.text = self._earliestDatestamp.strftime(constants.granularities[self._granularity])
        container.append(temp)
        
        # add deletedRecord
        temp = etree.Element(u'deletedRecord')
        temp.text = self._deletedRecord
        container.append(temp)
        
        # add granularity
        temp = etree.Element(u'granularity')
        temp.text = self._granularity
        container.append(temp)
        
        # add descriptions
        for d in self._description:
            temp = etree.Element(u'description')
            temp.text = d
            container.append(temp)
    
    def render_ListIdentifiers(self, request, root, request_tag):
        container = etree.Element(u'ListIdentifiers')
        token_tag, resources = self._listResources(request, root, request_tag)
        
        for resource in resources:
            self._renderResourceHeader(resource,container)
        
        if token_tag is not None:
            container.append(token_tag)
        
        root.append(container)
    
    def render_ListMetadataFormats(self, request, root, request_tag):
        formats = [{'metadataPrefix':u'oai_dc',
                    'schema':u'http://www.openarchives.org/OAI/2.0/oai_dc.xsd',
                    'metadataNamespace':u'http://www.openarchives.org/OAI/2.0/oai_dc/'},
                   {'metadataPrefix':u'marcxml',
                    'schema':u'http://www.loc.gov/standards/marcxml/schema/MARC21slim.xsd',
                    'metadataNamespace':u'http://www.loc.gov/MARC21/slim/'}]
        #TODO: handle the 'identifier' argument eventually...
        container = etree.Element(u'ListMetadataFormats')
        for format in formats:
            container.append(tags.MetadataFormat(**format))
        root.append(container)
    
    def render_ListRecords(self, request, root, request_tag):
        container = etree.Element(u'ListRecords')
        metadataPrefix = None
        
        if not 'metadataPrefix' in request.args:
            root.append(tags.Error('badArgument',
                                   freetext=u'A metadataPrefix must be provided'))
        elif len(request.args['metadataPrefix']) != 1:
            root.append(tags.Error('badArgument',
                                   freetext=u'Please provide only 1 metadataPrefix'))
        else:
            metadataPrefix = request.args['metadataPrefix'][0]
        
        if metadataPrefix:
            token_tag, resources = self._listResources(request, root, request_tag)
            
            deferreds = []
            
            for resource in resources:
                deferreds.append(self._renderResource(resource,container,
                                                      metadataPrefix))
            
            if token_tag is not None:
                container.append(token_tag)
            
            root.append(container)
            
            return defer.DeferredList(deferreds)
    
    def render_ListSets(self, request, root, request_tag):
        root.append(tags.Error('noSetHierarchy',
                               freetext=u'%s does not support sets at this '
                               'time' % (str(self._repositoryName),)))

class StorageWrapper(object):
    
    def __init__(self, storage, setSize=10, expires=True,
                 tokenLifetime=constants.defaultTokenLifetime,
                 datetime_format=constants.datetime_format, **kwargs):
        self._cursor = 0
        self._calls = 0
        self._expires = None
        
        if expires:
            self._expires = datetime.datetime.now() + tokenLifetime
        
        self._limit = setSize
        self._storage = storage
        self._datetime_format = datetime_format
        self._kwargs = kwargs
        self.id = temporary_id(['%s=%s' % (k,v) for k,v in kwargs.iteritems()])
    
    def __unicode__(self):
        return self.id
    
    def getTimesCalled(self):
        return self._calls
    
    #@property
    def expired(self):
        """ Return true if the wrapper expires and has expired """
        return self._expires is not None and \
               datetime.datetime.now() >self._expires
    
    expired = property(expired,doc=expired.__doc__)
    
    def getTag(self):
        return tags.ResumptionToken(self.id, cursor=self._cursor, 
                                    expirationDate=self._expires.strftime(self._datetime_format))
    
    tag = property(getTag,doc=getTag.__doc__)
    
    def getNextSet(self):
        self._calls += 1
        r = self._storage.getResources(inactive=True,limit=self._limit,
                                       offset=self._cursor, **self._kwargs)
        t = self.tag
        self._cursor += self._limit
        return t,r

"""
<oai_dc:dc xmlns:oai_dc="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:dc="http://purl.org/dc/elements/1.1/" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/ http://www.openarchives.org/OAI/2.0/OAI-PMH.xsd">
<dc:title>Zeitschrift fur elektrische Informations- und Energietechnik.</dc:title>
<dc:type>text</dc:type>
<dc:publisher>Leipzig, Geest &amp; Portig,</dc:publisher>
<dc:date>1971-1983.</dc:date>
<dc:language>ger</dc:language>
<dc:subject>Electric engineering</dc:subject>
<dc:subject>Electronics</dc:subject>
<dc:relation>Hochfrequenztechnik und elektroakustikj jahrbuch der drahtlosen telegraphie und telephonie.</dc:relation>
<dc:type/>
<dc:language/>
</oai_dc:dc>
"""